package jmine.tec.web.servlet.filter;

import static bancosys.tec.rtm.diagnostician.AbstractDiagnostician.JMX_DEFAULT_CACHE_DURATION_IN_SECONDS;
import static bancosys.tec.rtm.diagnostician.AbstractDiagnostician.JMX_NAMESPACE;
import static bancosys.tec.rtm.impl.diagnostician.MemoryUsageCalculatorDiagnostician.NAME;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeSet;

import jmine.tec.utils.debug.TimerGroup;
import jmine.tec.utils.debug.impl.TimerImpl;
import jmine.tec.utils.debug.impl.TimerImpl.ExecutionResult;

import org.apache.commons.lang.builder.CompareToBuilder;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;

import bancosys.tec.rtm.diagnostician.AbstractDiagnostician;
import bancosys.tec.rtm.diagnostician.DiagnosisBean;

/**
 * @author takeshi
 */
@ManagedResource(description = NAME, objectName = JMX_NAMESPACE + ":name=web-timer", currencyTimeLimit = JMX_DEFAULT_CACHE_DURATION_IN_SECONDS)
public class WebTimingDiagnostician extends AbstractDiagnostician implements BeanNameAware {

    private static final String AVERAGE_SPEED = "AVERAGE_SPEED";

    private static final double TWO_TO_TEN = 1024.0;

    private static final double THOUSAND = 1000.0;

    private static final String WORST_IO_AVERAGE_TIME = "WORST_IO_AVERAGE_TIME";

    private static final String WORST_IO_URI = "WORST_IO_URI";

    private static final String WORST_SERVICE_AVERAGE_TIME = "WORST_SERVICE_AVERAGE_TIME";

    private static final String WORST_SERVICE_URI = "WORST_SERVICE_URI";

    private static final String AVERAGE_IO_TIME = "AVERAGE_IO_TIME";

    private static final String AVERAGE_SERVICE_TIME = "AVERAGE_SERVICE_TIME";

    private static final String TOTAL_REQUESTS = "TOTAL_REQUESTS";

    private static final String OPEN_REQUESTS = "OPEN_REQUESTS";

    private TimingFilter filter;

    private String name = "WebTimingDiagnostician";

    private static final Comparator<ExecutionResult> AVERAGE_TIME_COMPARATOR = new Comparator<TimerImpl.ExecutionResult>() {

        public int compare(ExecutionResult o1, ExecutionResult o2) {
            return new CompareToBuilder().append(o1.getMean(), o2.getMean()).append(o1.getMax(), o2.getMax())
                    .append(o1.getInvocations(), o2.getInvocations()).append(o1.getSystemTime(), o2.getSystemTime()).toComparison();
        }
    };

    /**
     * {@inheritDoc}
     */
    public String getName() {
        return name;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean fillDiagnosisBean(DiagnosisBean bean) throws Exception {
        if (this.filter == null) {
            return false;
        }
        TimerGroup serviceTimer = this.filter.getServiceTimer();
        Collection<? extends String> names = new TreeSet<String>(serviceTimer.getExecutionNames());
        List<ExecutionResult> serviceResults = new ArrayList<TimerImpl.ExecutionResult>(names.size());
        List<ExecutionResult> ioResults = new ArrayList<TimerImpl.ExecutionResult>(names.size());
        addAllResults(bean, serviceTimer, names, serviceResults, ioResults);
        addOpenRequests(bean);
        addTotalServiceTime(bean, serviceTimer);
        addTotalIOTime(bean);
        addWorsts(bean, serviceResults, ioResults);
        return true;
    }

    /**
     * Adds the worst timings
     * 
     * @param bean the {@link DiagnosisBean}
     * @param serviceResults {@link List} of {@link ExecutionResult} for the service timer
     * @param ioResults {@link List} of {@link ExecutionResult} for the io timer
     */
    private void addWorsts(DiagnosisBean bean, List<ExecutionResult> serviceResults, List<ExecutionResult> ioResults) {
        Collections.sort(serviceResults, AVERAGE_TIME_COMPARATOR);
        Collections.sort(ioResults, AVERAGE_TIME_COMPARATOR);

        ExecutionResult worstServiceAverage = serviceResults.get(serviceResults.size() - 1);
        ExecutionResult worstIOAverage = ioResults.get(ioResults.size() - 1);
        bean.putValue(WORST_SERVICE_URI, worstServiceAverage.getName());
        bean.putValue(WORST_SERVICE_AVERAGE_TIME, worstServiceAverage.getMean());

        bean.putValue(WORST_IO_URI, worstIOAverage.getName());
        bean.putValue(WORST_IO_AVERAGE_TIME, worstIOAverage.getMean());
    }

    /**
     * Adds the total io time
     * 
     * @param bean {@link DiagnosisBean}
     */
    private void addTotalIOTime(DiagnosisBean bean) {
        ExecutionResult totalIOResult = this.filter.getIoTimer().mergeTimers();
        bean.addDescription("io average time: " + totalIOResult);
        bean.putValue(AVERAGE_IO_TIME, totalIOResult.getMean());
        double avgSpeed = (double) (THOUSAND * this.filter.getTotalBytesTransfered()) / (totalIOResult.getSystemTime() * TWO_TO_TEN);
        bean.addDescription("average io speed: " + avgSpeed + " KB/s");
        bean.putValue(AVERAGE_SPEED, avgSpeed);
    }

    /**
     * Adds the total service time
     * 
     * @param bean {@link DiagnosisBean}
     * @param serviceTimer {@link TimerGroup}
     */
    private void addTotalServiceTime(DiagnosisBean bean, TimerGroup serviceTimer) {
        ExecutionResult totalServiceResult = serviceTimer.mergeTimers();
        bean.addDescription("total service: " + totalServiceResult);
        bean.putValue(AVERAGE_SERVICE_TIME, totalServiceResult.getMean());
        bean.putValue(TOTAL_REQUESTS, totalServiceResult.getInvocations());
    }

    /**
     * Adds the open request count and URI's
     * 
     * @param bean {@link DiagnosisBean}
     */
    private void addOpenRequests(DiagnosisBean bean) {
        Map<Thread, String> map = this.filter.getOpenRequests();
        bean.addDescription("open requests: " + map.size());
        bean.putValue(OPEN_REQUESTS, map.size());
        for (Entry<Thread, String> entry : map.entrySet()) {
            bean.addDescription(entry.getValue());
        }
    }

    /**
     * Adds all the results from the {@link #filter} to the given lists, also filling out the {@link DiagnosisBean}
     * 
     * @param bean {@link DiagnosisBean}
     * @param serviceTimer {@link TimerGroup} for the request service
     * @param names {@link Collection} of {@link String}
     * @param serviceResults {@link List} to be filled
     * @param ioResults {@link List} to be filled
     */
    private void addAllResults(DiagnosisBean bean, TimerGroup serviceTimer, Collection<? extends String> names,
            List<ExecutionResult> serviceResults, List<ExecutionResult> ioResults) {
        for (String string : names) {
            bean.addDescription("Timers for: " + string);
            ExecutionResult serviceResult = serviceTimer.getResult(string);
            if (serviceResult != null) {
                serviceResults.add(serviceResult);
            }
            ExecutionResult ioResult = this.filter.getIoTimer().getResult(string);
            if (ioResult != null) {
                ioResults.add(ioResult);
            }

            bean.addDescription("request service timer: " + serviceResult);
            bean.addDescription("io timer: " + ioResult);
            bean.addDescription("---------------------------------------------------------------");
        }
    }

    /**
     * @return double the average upload speed in KB/s
     */
    @ManagedAttribute(description = AVERAGE_SPEED)
    public Double getAverageSpeed() {
        return (Double) getValue(AVERAGE_SPEED);
    }

    /**
     * @return the total number of requests monitored by the {@link #filter}
     */
    @ManagedAttribute(description = TOTAL_REQUESTS)
    public Long getTotalRequests() {
        return (Long) getValue(TOTAL_REQUESTS);
    }

    /**
     * @return the average time (in ms) that it has taken to process a request
     */
    @ManagedAttribute(description = AVERAGE_SERVICE_TIME)
    public Long getAverageServiceTime() {
        return (Long) getValue(AVERAGE_SERVICE_TIME);
    }

    /**
     * @return the average time (in ms) that it has taken to transfer the data to the client
     */
    @ManagedAttribute(description = AVERAGE_IO_TIME)
    public Long getAverageIOTime() {
        return (Long) getValue(AVERAGE_IO_TIME);
    }

    /**
     * @return the url that takes the longes to process (in average)
     */
    @ManagedAttribute(description = WORST_SERVICE_URI)
    public String getWorstServiceURI() {
        return (String) getValue(WORST_SERVICE_URI);
    }

    /**
     * @return the longest average time to process
     */
    @ManagedAttribute(description = WORST_SERVICE_AVERAGE_TIME)
    public Long getWorstServiceAverageTime() {
        return (Long) getValue(WORST_SERVICE_AVERAGE_TIME);
    }

    /**
     * @return the uri that takes the longes to transfer data (in average)
     */
    @ManagedAttribute(description = WORST_IO_URI)
    public String getWorstIOURI() {
        return (String) getValue(WORST_IO_URI);
    }

    /**
     * @return the average time of {@link #getWorstIOURI()}
     */
    @ManagedAttribute(description = WORST_IO_AVERAGE_TIME)
    public Long getWorstIOAverageTime() {
        return (Long) getValue(WORST_IO_AVERAGE_TIME);
    }

    /**
     * @return the number of requests being processed right now
     */
    @ManagedAttribute(description = OPEN_REQUESTS)
    public Integer openRequests() {
        return (Integer) getValue(OPEN_REQUESTS);
    }

    /**
     * {@inheritDoc}
     */
    public void setBeanName(String beanName) {
        this.name = beanName;
    }

    /**
     * @return the filter
     */
    public TimingFilter getFilter() {
        return filter;
    }

    /**
     * @param filter the filter to set
     */
    public void setFilter(TimingFilter filter) {
        this.filter = filter;
    }

}
